from collections import defaultdict
from datetime import date, datetime, timedelta
from typing import Callable, List, Dict, Optional, Type
from functools import lru_cache, partial
import traceback

import numpy as np
import pandas as pd
from pandas import DataFrame, Series
from pandas.core.window import ExponentialMovingWindow
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from vnpy.trader.constant import (
    Direction,
    Offset,
    Exchange,
    Interval,
    Status
)
from vnpy.trader.database import get_database, BaseDatabase
from vnpy.trader.object import OrderData, TradeData, BarData, TickData
from vnpy.trader.utility import round_to, extract_vt_symbol
from vnpy.trader.optimize import (
    OptimizationSetting,
    check_optimization_setting,
    run_bf_optimization,
    run_ga_optimization
)

from .base import (
    BacktestingMode,
    EngineType,
    STOPORDER_PREFIX,
    StopOrder,
    StopOrderStatus,
    INTERVAL_DELTA_MAP
)
from .template import CtaTemplate
from .locale import _

import math
def round_down_to_thousand(num):
    """
    向下取舍到 1000 的最小单位
    :param num: 输入的数值
    :return: 向下取舍后的结果
    """
    return math.floor(num / 1000) * 1000


class BacktestingEngine:
    """"""

    engine_type: EngineType = EngineType.BACKTESTING
    gateway_name: str = "BACKTESTING"

    def __init__(self) -> None:
        """"""
        self.vt_symbol: str = ""
        self.symbol: str = ""
        self.exchange: Exchange = None
        self.start: datetime = None
        self.end: datetime = None
        self.rate: float = 0
        self.slippage: float = 0
        self.size: float = 1
        self.pricetick: float = 0
        self.capital: int = 1_000_000
        self.risk_free: float = 0
        self.annual_days: int = 240
        self.half_life: int = 120
        self.mode: BacktestingMode = BacktestingMode.BAR

        self.strategy_class: Type[CtaTemplate] = None
        self.strategy: CtaTemplate = None
        self.tick: TickData
        self.bar: BarData
        self.datetime: datetime = None

        self.interval: Interval = None
        self.days: int = 0
        self.callback: Callable = None
        self.history_data: list = []

        self.stop_order_count: int = 0
        self.stop_orders: Dict[str, StopOrder] = {}
        self.active_stop_orders: Dict[str, StopOrder] = {}

        self.limit_order_count: int = 0
        self.limit_orders: Dict[str, OrderData] = {}
        self.active_limit_orders: Dict[str, OrderData] = {}

        self.trade_count: int = 0
        self.trades: Dict[str, TradeData] = {}

        self.logs: list = []

        self.daily_results: Dict[date, DailyResult] = {}
        self.daily_df: DataFrame = None

        # 持仓盁亏初始化
        self.long_avg_cost = 0          #多头持仓均价
        self.short_avg_cost = 0         #空头持仓均价
        self.long_pos = 0               #多头仓位
        self.short_pos = 0              #空头仓位
        self.long_profit_total = 0      #多头总盈亏
        self.short_profit_total = 0     #空头总盈亏

        #净值指标
        self.net_value = 0
        self.net_value_list = []

        #月度盈亏参数
        self.last_month_date = None
        self.month_pnl = 0
        self.month_dict = {}

    def clear_data(self) -> None:
        """
        Clear all data of last backtesting.
        """
        self.strategy = None
        self.tick = None
        self.bar = None
        self.datetime = None

        self.stop_order_count = 0
        self.stop_orders.clear()
        self.active_stop_orders.clear()

        self.limit_order_count = 0
        self.limit_orders.clear()
        self.active_limit_orders.clear()

        self.trade_count = 0
        self.trades.clear()

        self.logs.clear()
        self.daily_results.clear()

    def set_parameters(
        self,
        vt_symbol: str,
        interval: Interval,
        start: datetime,
        rate: float,
        slippage: float,
        size: float,
        pricetick: float,
        capital: int = 0,
        end: datetime = None,
        mode: BacktestingMode = BacktestingMode.BAR,
        risk_free: float = 0,
        annual_days: int = 240,
        half_life: int = 120
    ) -> None:
        """"""
        self.mode = mode
        self.vt_symbol = vt_symbol
        self.interval = Interval(interval)
        self.rate = rate
        self.slippage = slippage
        self.size = size
        self.pricetick = pricetick
        self.start = start

        self.symbol, exchange_str = self.vt_symbol.split(".")
        self.exchange = Exchange(exchange_str)

        self.capital = capital

        if not end:
            end = datetime.now()
        self.end = end.replace(hour=23, minute=59, second=59)

        self.mode = mode
        self.risk_free = risk_free
        self.annual_days = annual_days
        self.half_life = half_life

    def add_strategy(self, strategy_class: Type[CtaTemplate], setting: dict) -> None:
        """"""
        self.strategy_class = strategy_class
        self.strategy = strategy_class(
            self, strategy_class.__name__, self.vt_symbol, setting
        )

    def load_data(self) -> None:
        """"""
        self.output(_("开始加载历史数据,合约:{}".format(self.vt_symbol)))

        if not self.end:
            self.end = datetime.now()

        if self.start >= self.end:
            self.output(_("起始日期必须小于结束日期"))
            return

        self.history_data.clear() # Clear previously loaded history data

        # Load 30 days of data each time and allow for progress update
        total_days: int = (self.end - self.start).days
        progress_days: int = max(int(total_days / 10), 1)
        progress_delta: timedelta = timedelta(days=progress_days)
        interval_delta: timedelta = INTERVAL_DELTA_MAP[self.interval]

        start: datetime = self.start
        end: datetime = self.start + progress_delta
        progress = 0

        while start < self.end:
            progress_bar: str = "#" * int(progress * 10 + 1)
            self.output(_("加载进度：{} [{:.0%}]").format(progress_bar, progress))

            end: datetime = min(end, self.end)  # Make sure end time stays within set range

            if self.mode == BacktestingMode.BAR:
                data: List[BarData] = load_bar_data(
                    self.symbol,
                    self.exchange,
                    self.interval,
                    start,
                    end
                )
            else:
                data: List[TickData] = load_tick_data(
                    self.symbol,
                    self.exchange,
                    start,
                    end
                )

            self.history_data.extend(data)

            if total_days > 0:
                progress += progress_days / total_days
            else:
                progress += progress_days / (total_days+1)
            progress = min(progress, 1)

            start = end + interval_delta
            end += progress_delta

        self.output(_("历史数据加载完成，数据量：{}").format(len(self.history_data)))

    def run_backtesting(self) -> None:
        """"""
        if self.mode == BacktestingMode.BAR:
            func = self.new_bar
        else:
            func = self.new_tick

        self.strategy.on_init()
        self.strategy.inited = True
        self.output(_("策略初始化完成"))

        self.strategy.on_start()
        self.strategy.trading = True
        self.output(_("开始回放历史数据"))

        total_size: int = len(self.history_data)
        batch_size: int = max(int(total_size / 20), 1)

        for ix, i in enumerate(range(0, total_size, batch_size)):
            batch_data: list = self.history_data[i: i + batch_size]
            for data in batch_data:
                try:
                    func(data)
                except Exception:
                    self.output(_("触发异常，回测终止"))
                    self.output(traceback.format_exc())
                    return

            progress = min(ix / 20, 1)
            progress_bar: str = "=" * (ix + 1)
            self.output(_("回放进度：{} [{:.0%}]").format(progress_bar, progress))

        self.strategy.on_stop()
        self.output(_("历史数据回放结束"))

    def calculate_result(self) -> DataFrame:
        """"""
        self.output(_("开始计算逐日盯市盈亏"))

        if not self.trades:
            self.output(_("回测成交记录为空"))

        # Add trade data into daily reuslt.
        for trade in self.trades.values():
            d: date = trade.datetime.date()
            daily_result: DailyResult = self.daily_results[d]
            daily_result.add_trade(trade)

        # Calculate daily result by iteration.
        pre_close = 0
        start_pos = 0

        for daily_result in self.daily_results.values():
            daily_result.calculate_pnl(
                pre_close,
                start_pos,
                self.size,
                self.rate,
                self.slippage
            )

            pre_close = daily_result.close_price
            start_pos = daily_result.end_pos

        # Generate dataframe
        results: defaultdict = defaultdict(list)

        for daily_result in self.daily_results.values():
            for key, value in daily_result.__dict__.items():
                results[key].append(value)

        self.daily_df = DataFrame.from_dict(results).set_index("date")

        self.output(_("逐日盯市盈亏计算完成"))
        return self.daily_df


    def calculate_trades_result(self, trades):
        """
        Deal with trade data
        """
        dt, direction, offset, price, volume = [], [], [], [], []
        for i in trades.values():
            dt.append(i.datetime)
            direction.append(i.direction.value)
            offset.append(i.offset.value)
            price.append(i.price)
            volume.append(i.volume)

        # Generate DataFrame with datetime, direction, offset, price, volume
        df = pd.DataFrame()
        df["direction"] = direction
        df["offset"] = offset
        df["price"] = price
        df["volume"] = volume

        df["current_time"] = dt
        df["last_time"] = df["current_time"].shift(1)

        # Calculate trade amount
        df["amount"] = df["price"] * df["volume"]
        df["acum_amount"] = df["amount"].cumsum()

        # Calculate pos, net pos(with direction), acumluation pos(with direction)
        def calculate_pos(df):
            if df["direction"] == "多":
                result = df["volume"]
            else:
                result = - df["volume"]

            return result
        df["pos"] = df.apply(calculate_pos, axis=1)

        df["net_pos"] = df["pos"].cumsum()
        df["acum_pos"] = df["volume"].cumsum()

        # Calculate trade result, acumulation result
        # ej: trade result(buy->sell) means (new price - old price) * volume
        df["result"] = -1 * df["pos"] * df["price"]
        df["acum_result"] = df["result"].cumsum()

        # Filter column data when net pos comes to zero
        def get_acum_trade_result(df):
            if df["net_pos"] == 0:
                return df["acum_result"]
        df["acum_trade_result"] = df.apply(get_acum_trade_result, axis=1)

        def get_acum_trade_volume(df):
            if df["net_pos"] == 0:
                return df["acum_pos"]
        df["acum_trade_volume"] = df.apply(get_acum_trade_volume, axis=1)   

        def get_acum_trade_duration(df):
            if df["net_pos"] == 0:
                return df["current_time"] - df["last_time"]
        df["acum_trade_duration"] = df.apply(get_acum_trade_duration, axis=1)  

        def get_acum_trade_amount(df):
            if df["net_pos"] == 0:
                return df["acum_amount"]
        df["acum_trade_amount"] = df.apply(get_acum_trade_amount, axis=1) 

        # Select row data with net pos equil to zero     
        df = df.dropna()

        return df

    def calculate_base_net_pnl(self, df, capital):
        """
        Calculate statistic base on net pnl
        """
        df["acum_pnl"] = df["net_pnl"].cumsum()
        df["balance"] = df["acum_pnl"] + capital
        df["return"] = np.log(
            df["balance"] / df["balance"].shift(1)
            ).fillna(0)
        # df["highlevel"] = ( df["balance"].rolling( min_periods=1, window=len(df), center=False).max() )

        if len(df) > 0:
            df["highlevel"] = df["balance"].rolling(min_periods=1, window=len(df), center=False).max()
        else:
            # 如果 DataFrame 为空，可以选择赋一个默认值
            df["highlevel"] = None


        df["drawdown"] = df["balance"] - df["highlevel"]
        df["ddpercent"] = df["drawdown"] / df["highlevel"] * 100

        df.reset_index(drop=True, inplace=True)
        
        return df


    def generate_trade_df(self, trades, size, rate, slippage, capital):
        """
        Calculate trade result from increment
        """
        df = self.calculate_trades_result(trades)

        trade_df = pd.DataFrame()
        trade_df["close_direction"] = df["direction"]
        trade_df["close_time"] = df["current_time"]
        trade_df["close_price"] = df["price"]
        trade_df["pnl"] = df["acum_trade_result"] - \
            df["acum_trade_result"].shift(1).fillna(0)
        
        trade_df["volume"] = df["acum_trade_volume"] - \
            df["acum_trade_volume"].shift(1).fillna(0)
        trade_df["duration"] = df["current_time"] - \
            df["last_time"]
        trade_df["turnover"] = df["acum_trade_amount"] - \
            df["acum_trade_amount"].shift(1).fillna(0)
        
        trade_df["commission"] = trade_df["turnover"] * rate
        trade_df["slipping"] = trade_df["volume"] * size * slippage

        trade_df["net_pnl"] = trade_df["pnl"] - \
            trade_df["commission"] - trade_df["slipping"]

        result = self.calculate_base_net_pnl(trade_df, capital)
        return result


    def calculate_statistics(self, df: DataFrame = None, output=True, html_name=None) -> dict:
        """"""

        from pyecharts.charts import (Bar,Line,Graph,Gauge,Page)#柱状图, 折线图, 关系图, 仪表盘,多图同表
        from pyecharts import options as opts
        import random


        df_total_trades_detail = self.generate_trade_df(self.trades, self.size, self.rate, self.slippage, self.capital)
         
        trade_count = len(df_total_trades_detail)
        win = df_total_trades_detail[df_total_trades_detail["net_pnl"] > 0]
        win_pnl_medio = win["net_pnl"].mean()
        win_count = len(win)

        loss = df_total_trades_detail[df_total_trades_detail["net_pnl"] < 0]
        loss_pnl_medio = loss["net_pnl"].mean()
        loss_count = len(loss)

        winning_rate = win_count / trade_count if trade_count > 0 else 0
        win_loss_pnl_ratio = - win_pnl_medio / loss_pnl_medio if loss_pnl_medio != 0 else 0

        self.output(_("开始计算策略统计指标"))

        # Check DataFrame input exterior
        if df is None:
            df: DataFrame = self.daily_df

        # Init all statistics default value
        start_date: str = ""
        end_date: str = ""
        total_days: int = 0
        profit_days: int = 0
        loss_days: int = 0
        end_balance: float = 0
        max_drawdown: float = 0
        max_ddpercent: float = 0
        max_drawdown_start: str = ""
        max_drawdown_end: str = ""
        max_drawdown_duration: int = 0   
        total_net_pnl: float = 0
        total_net_pnl: float = 0
        daily_net_pnl: float = 0
        total_commission: float = 0
        daily_commission: float = 0
        total_slippage: float = 0
        daily_slippage: float = 0
        total_turnover: float = 0
        daily_turnover: float = 0
        total_trade_count: int = 0
        daily_trade_count: float = 0
        total_return: float = 0
        annual_return: float = 0
        daily_return: float = 0
        return_std: float = 0
        sharpe_ratio: float = 0
        ewm_sharpe: float = 0
        return_drawdown_ratio: float = 0

        # Check if balance is always positive
        positive_balance: bool = False

        if df is not None:
            # Calculate balance related time series data
            df["balance"] = df["net_pnl"].cumsum() + self.capital

            # When balance falls below 0, set daily return to 0
            pre_balance: Series = df["balance"].shift(1)
            pre_balance.iloc[0] = self.capital
            x = df["balance"] / pre_balance
            x[x <= 0] = np.nan
            df["return"] = np.log(x).fillna(0)

            df["highlevel"] = df["balance"].rolling(min_periods=1, window=len(df), center=False).max()
            df["drawdown"] = df["balance"] - df["highlevel"]
            df["ddpercent"] = df["drawdown"] / df["highlevel"] * 100

            # All balance value needs to be positive
            positive_balance = (df["balance"] > 0).all()
            if not positive_balance:
                self.output(_("回测中出现爆仓(资金小于等于0),无法计算策略统计指标"))

        # Calculate statistics value
        if positive_balance:
            # Calculate statistics value
            start_date = df.index[0]
            end_date = df.index[-1]

            total_days: int = len(df)
            profit_days: int = len(df[df["net_pnl"] > 0])
            loss_days: int = len(df[df["net_pnl"] < 0])
            
            end_balance = df["balance"].iloc[-1]
            max_drawdown = df["drawdown"].min()
            max_ddpercent = df["ddpercent"].min()
            max_drawdown_end = df["drawdown"].idxmin()

            if isinstance(max_drawdown_end, date):
                max_drawdown_start = df["balance"][:max_drawdown_end].idxmax()
                max_drawdown_duration: int = (max_drawdown_end - max_drawdown_start).days
            else:
                max_drawdown_duration: int = 0
                max_drawdown_start = ""
                max_drawdown_end = ""

            total_net_pnl: float = df["net_pnl"].sum()
            daily_net_pnl: float = total_net_pnl / total_days

            total_commission: float = df["commission"].sum()
            daily_commission: float = total_commission / total_days

            total_slippage: float = df["slippage"].sum()
            daily_slippage: float = total_slippage / total_days

            total_turnover: float = df["turnover"].sum()
            daily_turnover: float = total_turnover / total_days

            total_trade_count: int = df["trade_count"].sum()
            daily_trade_count: int = total_trade_count / total_days

            total_return: float = (end_balance / self.capital - 1) * 100
            annual_return: float = total_return / total_days * self.annual_days
            daily_return: float = df["return"].mean() * 100
            return_std: float = df["return"].std() * 100

            if return_std:
                daily_risk_free: float = self.risk_free / np.sqrt(self.annual_days)
                sharpe_ratio: float = (daily_return - daily_risk_free) / return_std * np.sqrt(self.annual_days)

                ewm_window: ExponentialMovingWindow = df["return"].ewm(halflife=self.half_life)
                ewm_mean: Series = ewm_window.mean() * 100
                ewm_std: Series = ewm_window.std() * 100
                # ewm_sharpe: float = ((ewm_mean - daily_risk_free) / ewm_std)[-1] * np.sqrt(self.annual_days)
                ewm_sharpe: float = ((ewm_mean - daily_risk_free) / ewm_std).iloc[-1] * np.sqrt(self.annual_days)
            else:
                sharpe_ratio: float = 0
                ewm_sharpe: float = 0

            if max_ddpercent:
                return_drawdown_ratio: float = -total_return / max_ddpercent
            else:
                return_drawdown_ratio = 0

        # Output
        if output:
            self.output("-" * 30)
            self.output(_("首个交易日：\t{}").format(start_date))
            self.output(_("最后交易日：\t{}").format(end_date))

            self.output(_("总交易日：\t{}").format(total_days))
            self.output(_("盈利交易日：\t{}").format(profit_days))
            self.output(_("亏损交易日：\t{}").format(loss_days))

            self.output(_("起始资金：\t{:,.2f}").format(self.capital))
            self.output(_("结束资金：\t{:,.2f}").format(end_balance))

            self.output(_("总收益率：\t{:,.2f}%").format(total_return))
            self.output(_("年化收益：\t{:,.2f}%").format(annual_return))
            self.output(_("最大回撤: \t{:,.2f}").format(max_drawdown))
            self.output(_("百分比最大回撤: {:,.2f}%").format(max_ddpercent))
            self.output(_("最大回撤天数: \t{}").format(max_drawdown_duration))

            self.output(_("总盈亏：\t{:,.2f}").format(total_net_pnl))
            self.output(_("总手续费：\t{:,.2f}").format(total_commission))
            self.output(_("总滑点：\t{:,.2f}").format(total_slippage))
            self.output(_("总成交金额：\t{:,.2f}").format(total_turnover))
            self.output(_("总成交笔数：\t{}").format(total_trade_count))

            self.output(_("总成交次数：\t{:,.2f}").format(trade_count))
            self.output(_("盈利成交次数：\t{:,.2f}").format(win_count))
            self.output(_("亏损成交次数：\t{:,.2f}").format(loss_count))
            self.output(_("胜率：        \t{:,.2f}%").format(winning_rate * 100))
            self.output(_("盈亏比：\t{:,.2f}").format(win_loss_pnl_ratio))


            self.output(_("日均盈亏：\t{:,.2f}").format(daily_net_pnl))
            self.output(_("日均手续费：\t{:,.2f}").format(daily_commission))
            self.output(_("日均滑点：\t{:,.2f}").format(daily_slippage))
            self.output(_("日均成交金额：\t{:,.2f}").format(daily_turnover))
            self.output(_("日均成交笔数：\t{}").format(daily_trade_count))

            self.output(_("日均收益率：\t{:,.2f}%").format(daily_return))
            self.output(_("收益标准差：\t{:,.2f}%").format(return_std))
            self.output(f"Sharpe Ratio：\t{sharpe_ratio:,.2f}")
            self.output(f"EWM Sharpe：\t{ewm_sharpe:,.2f}")
            self.output(_("收益回撤比：\t{:,.2f}").format(return_drawdown_ratio))

            #----------------------------------------------------------------------
            
            #pyecharts绘图写入html,mark_point标记点, mark_point_symbol标记点图形'circle', 'diamond', 'rounddiamond', 'triangle','pin', 'arrow'可选
            trades_list = []
            for trade_class in df['trades']:
                if trade_class:
                    for trade in trade_class:
                        trades_list.append(trade.__dict__)

            self.net_value_list = []
            for index in range(len(df['balance'])):
                if index == 0:
                    nets_pnl = 1
                else:
                    nets_pnl = df['balance'].iloc[index]/df['balance'].iloc[index - 1] - 1
                    # nets_pnl = df['balance'].iloc[index]/df['balance'].iloc[index-1]-1
                self.net_value += nets_pnl
                self.net_value_list.append(round(float(self.net_value),3))

            
            bar_1 = Bar()
            bar_1.add_xaxis(df['balance'].index.tolist())
            title_str = f"策略:{self.vt_symbol}_{self.strategy.strategy_name}\n起止时间：{df['balance'].index[0]}至{df['balance'].index[-1]}"
            bar_1.add_yaxis(title_str,df['balance'].tolist())      #主标题
            #主标题
            # bar_1.set_global_opts(opts.TitleOpts(title=f"资金\n\n总收益率：{total_return:,.3f}%"),toolbox_opts=opts.ToolboxOpts(),yaxis_opts=opts.AxisOpts(min_=round_down_to_thousand(df['balance'].min())))  #副标题, ToolboxOpts设置工具箱配置项
            bar_1.set_global_opts(opts.TitleOpts(title=f"资金\n\n总收益率：{total_return:,.3f}%"),toolbox_opts=opts.ToolboxOpts())  #副标题, ToolboxOpts设置工具箱配置项 
            bar_1.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项 
            #成交记录画图
            trade_datetime =  []
            trade_price = []
            for trade in trades_list:
                trade_datetime.append(trade["datetime"])
                trade_price.append(trade["price"])
            trades_opts_data = [opts.MarkPointItem(
                name = f"orderid:{trade['orderid']}, 标的：{trade['vt_symbol']}, 方向：{trade['direction'].value}, {trade['offset'].value}, 价格：{trade['price']}, 成交量：{trade['volume']}",             #成交详细信息添加到name
                itemstyle_opts = opts.ItemStyleOpts(color= "#ec0000" if trade["direction"].value == "多" else "#00da3c"),
                coord = [trade["datetime"],trade["price"] * random.randrange(1000,1010) / 1000],        #标注的坐标
                value = trade["direction"].value + trade["offset"].value
            ) for trade in trades_list]

            bar_2 = Line()
            bar_2.add_xaxis(trade_datetime)
            bar_2.add_yaxis(f"交易价格：交易时间：{trade_datetime[0]}至{trade_datetime[-1]}\n\n成交笔数：{len(trades_list)}",trade_price)      #主标题
            bar_2.set_global_opts(opts.TitleOpts(title="交易记录"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项
            bar_2.set_series_opts(label_opts=opts.LabelOpts(is_show=False),             #标签配置项
                    markpoint_opts = opts.MarkPointOpts(data = trades_opts_data,
                    #标记的图形圆形："circle'", 方形："rect'",  圆角方形："roundRect'", 三角形："triangle'", 菱形："diamond'", 水滴："pin'", 箭头：'arrow'
                    symbol = "pin"
                ),
                itemstyle_opts = opts.ItemStyleOpts(color = "#ec0000",color0 = "#00da3c"),
            )  #系列配置项 


            bar_3 = Bar()
            bar_3.add_xaxis(df['balance'].index.tolist())
            bar_3.add_yaxis(f"复利净值最高点：{max(self.net_value_list)}\t复利净值最低点：{min(self.net_value_list)}",self.net_value_list)
            bar_3.set_global_opts(opts.TitleOpts(title="复利净值"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项   
            bar_3.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项                  

            bar_4 = Bar()
            bar_4.add_xaxis(df['drawdown'].index.tolist())
            bar_4.add_yaxis(f"回撤资金\n\n最大回撤资金：{max_drawdown:,.3f}\n最大回撤日期: \t{max_drawdown_start}至{max_drawdown_end},最大回撤天数: \t{max_drawdown_duration}",df['drawdown'].tolist())
            bar_4.set_global_opts(opts.TitleOpts(title="资金"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项   
            bar_4.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项     

            bar_5 = Bar()
            bar_5.add_xaxis(df['ddpercent'].index.tolist())
            bar_5.add_yaxis(f"回撤百分比\n\n最大回撤率：{max_ddpercent:,.3f}%",df['ddpercent'].tolist())
            bar_5.set_global_opts(opts.TitleOpts(title="回撤百分比"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项   
            bar_5.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项     

            bar_6 = Bar()
            bar_6.add_xaxis(df['net_pnl'].index.tolist())
            bar_6.add_yaxis(f"日盈亏\n\n最大日盈利：{df['net_pnl'].max():,.3f}\n\n最大日亏损：{df['net_pnl'].min():,.3f}",df['net_pnl'].tolist())
            bar_6.set_global_opts(opts.TitleOpts(title="日盈亏"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项   
            bar_6.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项  

           

            hist,bin_edges= np.histogram(df['net_pnl'], bins=50)                       
            bar_8 = Bar()
            bar_8.add_xaxis(bin_edges[1:].tolist())
            bar_8.add_yaxis("盈亏分布直方图",hist.tolist())
            bar_8.set_global_opts(opts.TitleOpts(title="频数"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项   
            bar_8.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项  

            bar_9 = Bar()
            bar_9.add_xaxis(df['commission'].index.tolist())
            bar_9.add_yaxis(f"每日手续费\n\n日最高手续费:{df['commission'].max():,.3f}",df['commission'].tolist())
            bar_9.set_global_opts(opts.TitleOpts(title="手续费"),toolbox_opts=opts.ToolboxOpts())  #设置工具箱配置项
            bar_9.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  #系列配置项 

            page = Page()
            page.add(bar_1)
            page.add(bar_2)
            page.add(bar_3)
            page.add(bar_4)
            page.add(bar_5)
            page.add(bar_6)
            # page.add(bar_7)
            page.add(bar_8)
            page.add(bar_9)
            #图表结果保存为html
            # if html_name is None:
            #     html_name = f"F:\\backtesting_result\\{self.symbol}_{self.interval.value}_{df['balance'].index[0]}_{df['balance'].index[-1]}.html"
            # page.render(html_name)
            #保存回测统计数据到CSV
            # df.to_csv(html_name.replace('.html','.csv').replace(self.symbol,'daily_'+self.symbol), encoding='utf_8_sig')   

        statistics: dict = {
            "start_date": start_date,
            "end_date": end_date,
            "total_days": total_days,
            "profit_days": profit_days,
            "loss_days": loss_days,
            "capital": self.capital,
            "end_balance": end_balance,
            "max_drawdown": max_drawdown,
            "max_ddpercent": max_ddpercent,
            "max_drawdown_duration": max_drawdown_duration,
            "total_net_pnl": total_net_pnl,
            "daily_net_pnl": daily_net_pnl,
            "total_commission": total_commission,
            "daily_commission": daily_commission,
            "total_slippage": total_slippage,
            "daily_slippage": daily_slippage,
            "total_turnover": total_turnover,
            "daily_turnover": daily_turnover,
            "total_trade_count": total_trade_count,
            "trade_count": trade_count,
            "win_count": win_count,
            "winning_rate": winning_rate,
            "win_loss_pnl_ratio": win_loss_pnl_ratio,
            "daily_trade_count": daily_trade_count,
            "total_return": total_return,
            "annual_return": annual_return,
            "daily_return": daily_return,
            "return_std": return_std,
            "sharpe_ratio": sharpe_ratio,
            "ewm_sharpe": ewm_sharpe,
            "return_drawdown_ratio": return_drawdown_ratio,
        }

        # Filter potential error infinite value
        for key, value in statistics.items():
            if value in (np.inf, -np.inf):
                value = 0
            statistics[key] = np.nan_to_num(value)

        self.output(_("策略统计指标计算完成"))
        return statistics

    # def show_chart(self, df: DataFrame = None) -> None:
    def show_chart(self, df: DataFrame = None) -> go.Figure:
        """"""
        # Check DataFrame input exterior
        if df is None:
            df: DataFrame = self.daily_df

        # Check for init DataFrame
        if df is None:
            return

        fig = make_subplots(
            rows=4,
            cols=1,
            subplot_titles=["Balance", "Drawdown", "Daily Pnl", "Pnl Distribution"],
            vertical_spacing=0.06
        )

        balance_line = go.Scatter(
            x=df.index,
            y=df["balance"],
            mode="lines",
            name="Balance"
        )

        drawdown_scatter = go.Scatter(
            x=df.index,
            y=df["drawdown"],
            fillcolor="red",
            fill='tozeroy',
            mode="lines",
            name="Drawdown"
        )
        pnl_bar = go.Bar(y=df["net_pnl"], name="Daily Pnl")
        pnl_histogram = go.Histogram(x=df["net_pnl"], nbinsx=100, name="Days")

        fig.add_trace(balance_line, row=1, col=1)
        fig.add_trace(drawdown_scatter, row=2, col=1)
        fig.add_trace(pnl_bar, row=3, col=1)
        fig.add_trace(pnl_histogram, row=4, col=1)

        fig.update_layout(height=1000, width=1000)
        return fig

    def run_bf_optimization(
        self,
        optimization_setting: OptimizationSetting,
        output: bool = True,
        max_workers: int = None
    ) -> list:
        """"""
        if not check_optimization_setting(optimization_setting):
            return

        evaluate_func: callable = wrap_evaluate(self, optimization_setting.target_name)
        results: list = run_bf_optimization(
            evaluate_func,
            optimization_setting,
            get_target_value,
            max_workers=max_workers,
            output=self.output
        )

        if output:
            for result in results:
                msg: str = _("参数：{}, 目标：{}").format(result[0], result[1])
                self.output(msg)

        return results

    run_optimization = run_bf_optimization

    def run_ga_optimization(
        self,
        optimization_setting: OptimizationSetting,
        output: bool = True,
        max_workers: int = None,
        ngen_size: int = 30
    ) -> list:
        """"""
        if not check_optimization_setting(optimization_setting):
            return

        evaluate_func: callable = wrap_evaluate(self, optimization_setting.target_name)
        results: list = run_ga_optimization(
            evaluate_func,
            optimization_setting,
            get_target_value,
            max_workers=max_workers,
            ngen_size=ngen_size,
            output=self.output
        )

        if output:
            for result in results:
                msg: str = _("参数：{}, 目标：{}").format(result[0], result[1])
                self.output(msg)

        return results

    def update_daily_close(self, price: float) -> None:
        """"""
        d: date = self.datetime.date()

        daily_result: Optional[DailyResult] = self.daily_results.get(d, None)
        if daily_result:
            daily_result.close_price = price
        else:
            self.daily_results[d] = DailyResult(d, price)

    def new_bar(self, bar: BarData) -> None:
        """"""
        self.bar = bar
        self.datetime = bar.datetime

        self.cross_limit_order()
        self.cross_stop_order()
        self.strategy.on_bar(bar)
        self.update_postion()           #更新持仓数据  
        self.update_daily_close(bar.close_price)

    def new_tick(self, tick: TickData) -> None:
        """"""
        self.tick = tick
        self.datetime = tick.datetime

        self.cross_limit_order()
        self.cross_stop_order()
        self.strategy.on_tick(tick)
        self.update_postion()           #更新持仓数据  
        self.update_daily_close(tick.last_price)

    def cross_limit_order(self) -> None:
        """
        Cross limit order with last bar/tick data.
        """
        if self.mode == BacktestingMode.BAR:
            long_cross_price = self.bar.low_price
            short_cross_price = self.bar.high_price
            long_best_price = self.bar.open_price
            short_best_price = self.bar.open_price
        else:
            long_cross_price = self.tick.ask_price_1
            short_cross_price = self.tick.bid_price_1
            long_best_price = long_cross_price
            short_best_price = short_cross_price

        for order in list(self.active_limit_orders.values()):
            # Push order update with status "not traded" (pending).
            if order.status == Status.SUBMITTING:
                order.status = Status.NOTTRADED
                self.strategy.on_order(order)

            # Check whether limit orders can be filled.
            long_cross: bool = (
                order.direction == Direction.LONG
                and order.price >= long_cross_price
                and long_cross_price > 0
            )

            short_cross: bool = (
                order.direction == Direction.SHORT
                and order.price <= short_cross_price
                and short_cross_price > 0
            )

            if not long_cross and not short_cross:
                # update by cfy 碰到跳空,开仓使用开盘价成交,平仓使用限价单价格(要判断是否可以有价格成交.比如碰到跳空就使用开盘价平仓)
                if self.mode == BacktestingMode.BAR:
                    if order.direction == Direction.LONG:
                        if order.offset == Offset.OPEN:
                            order.price = long_best_price
                        else:
                            if order.price <=self.bar.high_price and order.price >= self.bar.low_price:
                                pass
                            else:
                                order.price = long_cross_price
                        long_cross = True
                    else:
                        if order.offset == Offset.OPEN:
                            order.price = short_best_price
                        else:
                            if order.price <=self.bar.high_price and order.price >= self.bar.low_price:
                                pass
                            else:
                                order.price = short_cross_price
                        short_cross = True
                else:
                    continue

                # continue

            # Push order udpate with status "all traded" (filled).
            order.traded = order.volume
            order.status = Status.ALLTRADED
            self.strategy.on_order(order)

            if order.vt_orderid in self.active_limit_orders:
                self.active_limit_orders.pop(order.vt_orderid)

            # Push trade update
            self.trade_count += 1

            if long_cross:
                # trade_price = min(order.price, long_best_price)
                trade_price = order.price # update by cfy 使用限价单价格
                pos_change = order.volume
            else:
                # trade_price = max(order.price, short_best_price)
                trade_price = order.price # update by cfy 使用限价单价格
                pos_change = -order.volume

            trade: TradeData = TradeData(
                symbol=order.symbol,
                exchange=order.exchange,
                orderid=order.orderid,
                tradeid=str(self.trade_count),
                direction=order.direction,
                offset=order.offset,
                price=trade_price,
                volume=order.volume,
                datetime=self.datetime,
                gateway_name=self.gateway_name,
            )

            self.strategy.pos += pos_change
            self.strategy.on_trade(trade)

            self.trades[trade.vt_tradeid] = trade
            # 更新持仓数据
            self.update_postion(trade=trade)
    def cross_stop_order(self) -> None:
        """
        Cross stop order with last bar/tick data.
        """
        if self.mode == BacktestingMode.BAR:
            long_cross_price = self.bar.high_price
            short_cross_price = self.bar.low_price
            long_best_price = self.bar.open_price
            short_best_price = self.bar.open_price
        else:
            long_cross_price = self.tick.last_price
            short_cross_price = self.tick.last_price
            long_best_price = long_cross_price
            short_best_price = short_cross_price

        for stop_order in list(self.active_stop_orders.values()):
            # Check whether stop order can be triggered.
            long_cross: bool = (
                stop_order.direction == Direction.LONG
                and stop_order.price <= long_cross_price
            )

            short_cross: bool = (
                stop_order.direction == Direction.SHORT
                and stop_order.price >= short_cross_price
            )

            if not long_cross and not short_cross:
                continue

            # Create order data.
            self.limit_order_count += 1

            order: OrderData = OrderData(
                symbol=self.symbol,
                exchange=self.exchange,
                orderid=str(self.limit_order_count),
                direction=stop_order.direction,
                offset=stop_order.offset,
                price=stop_order.price,
                volume=stop_order.volume,
                traded=stop_order.volume,
                status=Status.ALLTRADED,
                gateway_name=self.gateway_name,
                datetime=self.datetime
            )

            self.limit_orders[order.vt_orderid] = order

            # Create trade data.
            if long_cross:
                trade_price = max(stop_order.price, long_best_price)
                pos_change = order.volume
            else:
                trade_price = min(stop_order.price, short_best_price)
                pos_change = -order.volume

            self.trade_count += 1

            trade: TradeData = TradeData(
                symbol=order.symbol,
                exchange=order.exchange,
                orderid=order.orderid,
                tradeid=str(self.trade_count),
                direction=order.direction,
                offset=order.offset,
                price=trade_price,
                volume=order.volume,
                datetime=self.datetime,
                gateway_name=self.gateway_name,
            )

            self.trades[trade.vt_tradeid] = trade

            # Update stop order.
            stop_order.vt_orderids.append(order.vt_orderid)
            stop_order.status = StopOrderStatus.TRIGGERED

            if stop_order.stop_orderid in self.active_stop_orders:
                self.active_stop_orders.pop(stop_order.stop_orderid)

            # Push update to strategy.
            self.strategy.on_stop_order(stop_order)
            self.strategy.on_order(order)

            self.strategy.pos += pos_change
            self.strategy.on_trade(trade)
            # 更新持仓数据
            self.update_postion(trade=trade)
    #----------------------------------------------------------------------
    def update_postion(self, trade =None):
        """持仓监控"""
        if trade:
            if trade.direction == Direction.LONG:
                # 做多单
                if trade.offset == Offset.OPEN:
                    long_cost = self.long_avg_cost * self.long_pos
                    long_cost += trade.price * trade.volume
                    # 平均成本
                    self.long_pos += trade.volume
                    if self.long_pos > 0:
                        self.long_avg_cost = round(long_cost / float(self.long_pos), 3)

                else:
                    self.short_pos -= trade.volume
            else:
                # 做空单
                if trade.offset == Offset.OPEN:
                    short_cost = self.short_avg_cost * self.short_pos
                    short_cost += trade.price * trade.volume
                    # 平均成本
                    self.short_pos += trade.volume
                    if self.short_pos > 0:
                        self.short_avg_cost = round(short_cost / float(self.short_pos), 3)
                else:
                    self.long_pos -= trade.volume
        # 多/空仓收益
        if self.mode == BacktestingMode.BAR:
            last_price = self.bar.close_price
        else:
            last_price = self.tick.last_price
        long_profit = (last_price - self.long_avg_cost) * self.long_pos * self.size
        short_profit = (self.short_avg_cost - last_price) * self.short_pos * self.size
        if trade:
            if trade.direction == Direction.LONG:
                self.long_profit_total += long_profit               
            if trade.direction == Direction.SHORT:
                self.short_profit_total += short_profit                            
        self.strategy.long_pos = self.long_pos
        self.strategy.short_pos = self.short_pos
        self.strategy.long_profit = long_profit
        self.strategy.short_profit = short_profit
        self.strategy.balance = self.capital + self.long_profit_total + self.short_profit_total


    def load_bar(
        self,
        vt_symbol: str,
        days: int,
        interval: Interval,
        callback: Callable,
        use_database: bool
    ) -> List[BarData]:
        """"""
        self.callback = callback

        init_end = self.start - INTERVAL_DELTA_MAP[interval]
        init_start = self.start - timedelta(days=days)

        symbol, exchange = extract_vt_symbol(vt_symbol)

        bars: List[BarData] = load_bar_data(
            symbol,
            exchange,
            interval,
            init_start,
            init_end
        )

        return bars

    def load_tick(self, vt_symbol: str, days: int, callback: Callable) -> List[TickData]:
        """"""
        self.callback = callback

        init_end = self.start - timedelta(seconds=1)
        init_start = self.start - timedelta(days=days)

        symbol, exchange = extract_vt_symbol(vt_symbol)

        ticks: List[TickData] = load_tick_data(
            symbol,
            exchange,
            init_start,
            init_end
        )

        return ticks

    def send_order(
        self,
        strategy: CtaTemplate,
        direction: Direction,
        offset: Offset,
        price: float,
        volume: float,
        stop: bool,
        lock: bool,
        net: bool
    ) -> list:
        """"""
        price: float = round_to(price, self.pricetick)
        if stop:
            vt_orderid: str = self.send_stop_order(direction, offset, price, volume)
        else:
            vt_orderid: str = self.send_limit_order(direction, offset, price, volume)
        return [vt_orderid]

    def send_stop_order(
        self,
        direction: Direction,
        offset: Offset,
        price: float,
        volume: float
    ) -> str:
        """"""
        self.stop_order_count += 1

        stop_order: StopOrder = StopOrder(
            vt_symbol=self.vt_symbol,
            direction=direction,
            offset=offset,
            price=price,
            volume=volume,
            datetime=self.datetime,
            stop_orderid=f"{STOPORDER_PREFIX}.{self.stop_order_count}",
            strategy_name=self.strategy.strategy_name,
        )

        self.active_stop_orders[stop_order.stop_orderid] = stop_order
        self.stop_orders[stop_order.stop_orderid] = stop_order

        return stop_order.stop_orderid

    def send_limit_order(
        self,
        direction: Direction,
        offset: Offset,
        price: float,
        volume: float
    ) -> str:
        """"""
        self.limit_order_count += 1

        order: OrderData = OrderData(
            symbol=self.symbol,
            exchange=self.exchange,
            orderid=str(self.limit_order_count),
            direction=direction,
            offset=offset,
            price=price,
            volume=volume,
            status=Status.SUBMITTING,
            gateway_name=self.gateway_name,
            datetime=self.datetime
        )

        self.active_limit_orders[order.vt_orderid] = order
        self.limit_orders[order.vt_orderid] = order

        return order.vt_orderid

    def cancel_order(self, strategy: CtaTemplate, vt_orderid: str) -> None:
        """
        Cancel order by vt_orderid.
        """
        if vt_orderid.startswith(STOPORDER_PREFIX):
            self.cancel_stop_order(strategy, vt_orderid)
        else:
            self.cancel_limit_order(strategy, vt_orderid)

    def cancel_stop_order(self, strategy: CtaTemplate, vt_orderid: str) -> None:
        """"""
        if vt_orderid not in self.active_stop_orders:
            return
        stop_order: StopOrder = self.active_stop_orders.pop(vt_orderid)

        stop_order.status = StopOrderStatus.CANCELLED
        self.strategy.on_stop_order(stop_order)

    def cancel_limit_order(self, strategy: CtaTemplate, vt_orderid: str) -> None:
        """"""
        if vt_orderid not in self.active_limit_orders:
            return
        order: OrderData = self.active_limit_orders.pop(vt_orderid)

        order.status = Status.CANCELLED
        self.strategy.on_order(order)

    def cancel_all(self, strategy: CtaTemplate) -> None:
        """
        Cancel all orders, both limit and stop.
        """
        vt_orderids: list = list(self.active_limit_orders.keys())
        for vt_orderid in vt_orderids:
            self.cancel_limit_order(strategy, vt_orderid)

        stop_orderids: list = list(self.active_stop_orders.keys())
        for vt_orderid in stop_orderids:
            self.cancel_stop_order(strategy, vt_orderid)

    def write_log(self, msg: str, strategy: CtaTemplate = None) -> None:
        """
        Write log message.
        """
        msg: str = f"{self.datetime}\t{msg}"
        self.logs.append(msg)

    def send_email(self, msg: str, strategy: CtaTemplate = None) -> None:
        """
        Send email to default receiver.
        """
        pass

    def sync_strategy_data(self, strategy: CtaTemplate) -> None:
        """
        Sync strategy data into json file.
        """
        pass

    def get_engine_type(self) -> EngineType:
        """
        Return engine type.
        """
        return self.engine_type

    def get_pricetick(self, strategy: CtaTemplate) -> float:
        """
        Return contract pricetick data.
        """
        return self.pricetick

    def get_size(self, strategy: CtaTemplate) -> int:
        """
        Return contract size data.
        """
        return self.size

    def put_strategy_event(self, strategy: CtaTemplate) -> None:
        """
        Put an event to update strategy status.
        """
        pass

    def output(self, msg) -> None:
        """
        Output message of backtesting engine.
        """
        print(f"{datetime.now()}\t{msg}")

    def get_all_trades(self) -> list:
        """
        Return all trade data of current backtesting result.
        """
        return list(self.trades.values())

    def get_all_orders(self) -> list:
        """
        Return all limit order data of current backtesting result.
        """
        return list(self.limit_orders.values())

    def get_all_daily_results(self) -> list:
        """
        Return all daily result data.
        """
        return list(self.daily_results.values())


class DailyResult:
    """"""

    def __init__(self, date: date, close_price: float) -> None:
        """"""
        self.date: date = date
        self.close_price: float = close_price
        self.pre_close: float = 0

        self.trades: List[TradeData] = []
        self.trade_count: int = 0

        self.start_pos = 0
        self.end_pos = 0

        self.turnover: float = 0
        self.commission: float = 0
        self.slippage: float = 0

        self.trading_pnl: float = 0
        self.holding_pnl: float = 0
        self.total_pnl: float = 0
        self.net_pnl: float = 0

    def add_trade(self, trade: TradeData) -> None:
        """"""
        self.trades.append(trade)

    def calculate_pnl(
        self,
        pre_close: float,
        start_pos: float,
        size: int,
        rate: float,
        slippage: float
    ) -> None:
        """"""
        # If no pre_close provided on the first day,
        # use value 1 to avoid zero division error
        if pre_close:
            self.pre_close = pre_close
        else:
            self.pre_close = 1

        # Holding pnl is the pnl from holding position at day start
        self.start_pos = start_pos
        self.end_pos = start_pos

        self.holding_pnl = self.start_pos * (self.close_price - self.pre_close) * size

        # Trading pnl is the pnl from new trade during the day
        self.trade_count = len(self.trades)

        for trade in self.trades:
            if trade.direction == Direction.LONG:
                pos_change = trade.volume
            else:
                pos_change = -trade.volume

            self.end_pos += pos_change

            turnover: float = trade.volume * size * trade.price
            self.trading_pnl += pos_change * \
                (self.close_price - trade.price) * size
            self.slippage += trade.volume * size * slippage

            self.turnover += turnover
            self.commission += turnover * rate

        # Net pnl takes account of commission and slippage cost
        self.total_pnl = self.trading_pnl + self.holding_pnl
        self.net_pnl = self.total_pnl - self.commission - self.slippage


@lru_cache(maxsize=999)
def load_bar_data(
    symbol: str,
    exchange: Exchange,
    interval: Interval,
    start: datetime,
    end: datetime
) -> List[BarData]:
    """"""
    database: BaseDatabase = get_database()

    return database.load_bar_data(
        symbol, exchange, interval, start, end
    )


@lru_cache(maxsize=999)
def load_tick_data(
    symbol: str,
    exchange: Exchange,
    start: datetime,
    end: datetime
) -> List[TickData]:
    """"""
    database: BaseDatabase = get_database()

    return database.load_tick_data(
        symbol, exchange, start, end
    )


def evaluate(
    target_name: str,
    strategy_class: CtaTemplate,
    vt_symbol: str,
    interval: Interval,
    start: datetime,
    rate: float,
    slippage: float,
    size: float,
    pricetick: float,
    capital: int,
    end: datetime,
    mode: BacktestingMode,
    setting: dict
) -> tuple:
    """
    Function for running in multiprocessing.pool
    """
    engine: BacktestingEngine = BacktestingEngine()

    engine.set_parameters(
        vt_symbol=vt_symbol,
        interval=interval,
        start=start,
        rate=rate,
        slippage=slippage,
        size=size,
        pricetick=pricetick,
        capital=capital,
        end=end,
        mode=mode
    )

    engine.add_strategy(strategy_class, setting)
    engine.load_data()
    engine.run_backtesting()
    engine.calculate_result()
    statistics: dict = engine.calculate_statistics(output=False)

    target_value: float = statistics[target_name]
    return (setting, target_value, statistics)


def wrap_evaluate(engine: BacktestingEngine, target_name: str) -> callable:
    """
    Wrap evaluate function with given setting from backtesting engine.
    """
    func: callable = partial(
        evaluate,
        target_name,
        engine.strategy_class,
        engine.vt_symbol,
        engine.interval,
        engine.start,
        engine.rate,
        engine.slippage,
        engine.size,
        engine.pricetick,
        engine.capital,
        engine.end,
        engine.mode
    )
    return func


def get_target_value(result: list) -> float:
    """
    Get target value for sorting optimization results.
    """
    return result[1]
